主题
Go 云原生工具选型
Docker、Kubernetes、Helm、Argo CD、GitHub Actions、ko、client-go、controller-runtime 经常出现在同一张架构图上,但它们没有在争夺同一份工作。真正需要决定的是:谁构建程序,谁保存部署期望,谁持续修复偏差,以及有没有必要自己写一个控制器。本篇按这些责任选工具,而不是建议一个 Go 服务把八个都装上。
实际验证使用 Go 1.27.2、Linux/amd64,以及固定的 client-go v0.37.1、controller-runtime v0.25.2。文末给出真实客户端库 fake 实验、完整依赖锁定和 RBAC 配置。fake 在内存中运行,不读取 kubeconfig,也不连接集群;其通过不等于 Kubernetes 部署、权限或控制器运行已经验证。
先找自己缺的是哪一层
| 工具 | 输入 → 输出 | 主要职责 | 它不替你完成什么 |
|---|---|---|---|
| Docker / BuildKit | Dockerfile 与构建上下文 → 镜像 | 通用镜像构建、容器开发与运行工具 | 多节点应用的持续协调 |
| ko | Go main 包与配置 → OCI 镜像 | 面向 Go 的简化镜像构建 | 任意系统包安装脚本或完整运行平台 |
| GitHub Actions | 仓库事件 → runner 中的一组 job | 检查、构建、发布流程编排 | 集群长期漂移检测,除非另写这套机制 |
| Helm | chart 与 values → 清单及可管理 release | 参数化打包与发布 | 自动让数据库形成高可用 |
| Argo CD | Git/声明来源 → 目标集群状态协调 | GitOps 持续比较、同步与健康展示 | 编译 Go 或取代应用监控 |
| Kubernetes | API 期望状态 → 工作负载运行与状态 | 调度、控制循环、服务发现等平台 | 业务幂等、数据备份与应用正确性 |
| client-go | Go API 调用 → Kubernetes API 交互 | typed/dynamic client、缓存与队列等基础能力 | 自动决定该做什么业务协调 |
| controller-runtime | 资源观察与 Reconcile → 控制器程序 | 组织 manager、cache、client、controller 等 | 自动生成正确的领域运维逻辑 |
正在绘制图示…
查看图源代码
flowchart LR
S[Go 源码] --> CI[GitHub Actions 编排检查]
CI --> B{构建选择}
B --> D[Docker / BuildKit]
B --> KO[ko]
D --> I[同一套 Registry 中的 OCI 制品]
KO --> I
I --> P[部署声明固定 digest]
H[Helm chart 与 values] --> P
P --> G[Argo CD 协调]
G --> K[Kubernetes API 与控制器]
C[自写 Go 工具:client-go] --> K
O[自写 Operator:controller-runtime] --> K
两条构建分支通常选其一;Argo CD 使用 Helm 时,常由 Helm 渲染清单,再由 Argo CD 管理应用生命周期,不能把它想成在集群里不停执行 helm upgrade 的同义包装。Argo CD 与 Helm
Docker 和 ko:构建自由度与项目约定的取舍
Dockerfile 适合包含多种语言、系统库、生成步骤和复杂文件布局的构建。多阶段构建可以把工具链留在 build stage,只把产物交给运行镜像。代价是团队要维护 Dockerfile、基础镜像、缓存顺序与构建安全;构建凭据需要 secret mount,不能进入镜像层。Docker 多阶段
ko 直接调用 Go 构建并组装镜像,不要求 Docker daemon。单个 Go 程序、少量 OS 依赖,是它更擅长的情况。常用入口是 ko build 构建,ko resolve 把清单中的 Go 引用解析为镜像引用;ko apply 还会进入部署动作,不能把它当成无副作用的渲染命令。ko 介绍、ko build
| 项目条件 | 倾向选择 | 原因与边界 |
|---|---|---|
| 单个纯 Go 服务,文件依赖少 | ko | 少维护 Dockerfile;仍须固定基础镜像与工具链 |
| 需要 apt/apk、C 库、复杂系统准备 | Dockerfile | 构建步骤可控;运行镜像 ABI 仍要匹配 |
| 构建中有多个语言工具链 | Dockerfile 或既有构建系统 | 不应强迫所有步骤塞进 Go 约定 |
| 希望 CI 不运行 Docker daemon | ko 或其他无 daemon 构建路线 | 无 daemon 不代表无 Registry 凭据、无网络或无供应链风险 |
ko 的静态资产约定是 kodata,运行时位置通过 KO_DATA_PATH 提供。若使用 Go embed,文件随二进制进入镜像;使用 kodata 则保留独立资产目录,两种方式影响更新和路径处理。ko 对 cgo、OS 包等复杂依赖的限制,需要在真正替换 Dockerfile 前检查。ko 静态资产、限制
不要忽略发布默认值:构建工具可能把镜像推到 KO_DOCKER_REPO 指定仓库。ko resolve 本身也会构建并默认推送,并非纯文本替换。想只做文件输出,应明确检查所用版本的 --push=false 与 OCI layout/tarball 选项;--local 指向本地 Docker daemon,也不等于只写 YAML。ko resolve本文没有运行 ko,没有下载基础镜像或推送 Registry;“不需要 Docker”不是执行这些外部动作的授权。
GitHub Actions 负责一次流程,Kubernetes 负责持续运行
Actions 把事件、job、依赖关系、runner 和权限组织起来。对 Go 服务,常用来跑 test/race/vet、构建一个已固定身份的制品,再记录发布证据。缓存用来加速,artifact 用来保存结果,Registry 用来分发镜像,这三者用途不同。完整流程见《Go CI-CD 与 GitOps》。
应把不可信 PR 验证与持有发布身份的 job 分开,限制 token 权限,固定 action commit,按组织规则约束 OIDC 身份。自托管 runner 的宿主访问权也属于安全边界,不要只检查 YAML 上有没有 secrets。GitHub Actions 安全使用
Kubernetes 则长期运行控制循环:Deployment 管理可替换 Pod,scheduler 放置实例,kubelet 与运行时启动容器。集群 API 与控制器提供的是平台能力,不保证应用能承受节点故障、读写兼容或数据库不可用。一个小服务若已有托管容器平台满足需求,不必为了“云原生”先维护自己的集群。Kubernetes 组件
二者可以合作而不互相替代。Actions 构建完就结束;Deployment controller 仍继续维持副本。Actions 也可以做部署,但若需要发现数小时后出现的手工漂移,必须安排持续协调者,而不是指望已经完成的 job 继续工作。
Helm 和 Argo CD:打包与长期所有权不要弄反
Helm 把一组 Kubernetes 清单包装成 chart,通过 values 改变实例参数。helm template 渲染出文本,helm install/upgrade 才进入实际 release 操作。渲染成功不代表目标集群有对应 CRD、权限或资源容量;含 lookup 的模板还涉及渲染环境差异。helm template
Chart version、应用版本和镜像 digest 是三件事。一个 chart 可以不改应用,只修 probe 或 RBAC;同一应用版本也可以用不同 chart 配置。因此升级评审应看渲染后的差异、具体镜像和权限变化,而不只看 Chart.yaml 的版本号。
Argo CD 持续比较声明来源与集群状态。repo-server 负责取得并生成清单,application-controller 负责比较和协调,API/UI 提供访问和管理入口。实际部署身份、目标命名空间、项目约束和 RBAC 决定它能改什么。Argo CD 架构
| 团队需要 | 合适组合 | 不必马上加的东西 |
|---|---|---|
| 手动、低频部署一组配置 | Helm 加审查和明确操作流程 | 为一次模板渲染装完整 GitOps 平台 |
| 多环境,要求持续检查漂移 | Helm/Kustomize 加 Argo CD | 再让 CI 同时写同一批对象 |
| 反复进行复杂领域运维 | 上述平台加专用 Operator | 只靠越来越多 Helm hooks 模拟持久状态机 |
给一个字段安排两个互不协调的所有者,结果往往是反复覆盖。例如 CI 直接改镜像、Argo CD 又从 Git 修回;Helm 强制写 replicas、HPA 再修改。应先明确对象与字段所有权,变更从同一条审查路径进入。Argo CD 的 prune 会涉及删除,self-heal 会覆盖现场漂移,启用前需确认期望行为和事故流程。自动同步策略
client-go:想写集群工具,先从普通客户端开始
client-go 提供与 Kubernetes API 交互的 Go 客户端。typed client 让 Deployment、ConfigMap 等内置资源有静态类型;dynamic client 适合按 GroupVersionResource 操作未编入程序的类型。它还提供 REST 配置、发现、informer、lister 和 workqueue 等控制器构件。client-go v0.37.1
| 任务 | 常用入口 | 需要记住的边界 |
|---|---|---|
| 一次查询或修改内置资源 | Clientset 的 typed API | context 超时、分页、错误分类、权限 |
| 操作不预先编译进程序的 CRD | dynamic client | 字段缺少编译期类型检查,schema 仍由服务端决定 |
| 持续观察很多对象 | SharedInformer / Lister | 本地缓存有延迟,需等待初始同步 |
| 合并事件并重试处理 | rate-limiting workqueue | 事件不是完整事实,业务动作仍需幂等 |
| 对外部集群连接 | 受控 kubeconfig 或集群内身份 | 不要把凭据写死,也不要自动读取未授权账户 |
频繁全量 list 会增加 API server 和客户端开销。informer 通常通过 list/watch 维护本地缓存,让读取更便宜;但缓存不是每次都去 API server 取最新状态。拿到共享缓存中的对象后不要原地改写,应复制或交给符合语义的客户端操作。watch 中断后还要重新建立观察,不能把“一条连接没断”作为正确性前提。
客户端的 QPS/Burst 设置限制本客户端请求速率,不等于集群的全局配额。遇到 Conflict 应重新读取并重试适当更新;Forbidden 表示权限问题,盲目高频重试不会改善;NotFound 是否正常,则取决于正在协调创建还是删除。
本文真实调用了 client-go 的 typed API,但对象放在它自己的 fake tracker 中:
go
cs := clientgofake.NewClientset(cm.DeepCopy())
list, err := cs.CoreV1().ConfigMaps("demo").List(ctx, metav1.ListOptions{})text
client-go fake: configmaps= 1这证明 API 用法在固定版本上可编译执行,不证明网络、认证、RBAC 或 server-side defaulting 正确。client-go 的 NewClientset 源码明确说明它使用简化对象 tracker,不能替代真实 API server。
controller-runtime:当你确实要长期协调资源
只做一次查询,无需引入 manager、cache 和 controller。需要根据自定义资源长期创建、更新、删除其他对象时,controller-runtime 能把这些组件组织起来:manager 管理共享组件和生命周期,controller 监听对象并把 key 放入队列,Reconciler 读取当前状态并执行协调。controller-runtime
正在绘制图示…
查看图源代码
flowchart LR
A[API server list/watch] --> C[Cache / Informer]
C --> E[事件映射为 namespace/name]
E --> Q[去重与重试队列]
Q --> R[Reconcile 重新读取当前状态]
R --> W[按需要写 API 或外部系统]
W --> A
R -->|普通错误| Q
Reconcile 收到的是资源标识,不是某次事件的全部内容。事件可能合并,也可能在处理前又发生变化,因此应问“现在还差什么”,而不是“刚才那一次事件让我加几份”。v0.25.2 Reconcile 源码
本文的最小协调逻辑只给指定 ConfigMap 的 Data 增加 managed=true,保留其他键。关键条件是:已经满足期望时不写回。
go
if cm.Data["managed"] == "true" {
return reconcile.Result{}, nil
}
if cm.Data == nil { cm.Data = map[string]string{} }
cm.Data["managed"] = "true"
return reconcile.Result{}, r.Client.Update(ctx, &cm)连续调用两次的实际输出:
text
two reconciles: updates=1 managed=true owner=team第一次补字段,第二次观察到期望已满足,不再 Update;原有 owner 没丢。若每次都写“当前时间”或无条件修改对象,可能使自己的 watch 不断触发,形成无意义循环。
测试另外注入一次 Conflict:第一次返回错误,第二次重新 Get 后成功。这里第二次是测试明确调用,不是框架队列自动重试的实测。按固定版本 Reconcile 契约,普通非 nil error 会使 controller 通过退避重排;TerminalError 是不再重排的例外,等待已知外部事件可使用 RequeueAfter。业务需要的重试策略不能只靠“所有错误全部吞掉”。
缓存和并发是控制器容易遗漏的成本
manager 的常用 client 读缓存、写 API,因此写成功后的立刻 Get 未必立即看到新状态。需要强一致读取的特定步骤可以选择直接 APIReader,但大量绕过缓存又会提高 API 压力。不要把 fake client 里立刻可见的结果当成真实 cache 的保证。controller-runtime FAQ
控制器通常按不同资源 key 并发协调。同一逻辑可能在重启、leader 切换或失败后重复运行。对云资源创建、发送通知、支付等外部副作用,应另设幂等键和状态恢复机制;leader election 不等于 exactly-once。finalizer 可以延迟删除以清理外部资源,但也会因控制器失效阻塞删除,必须有可诊断状态与人工恢复路径。
fake client 的限制非常关键:它不做完整 OpenAPI validation,resourceVersion、generation、子资源等行为与真实 API 有差异。更接近 API 语义的下一层是 envtest,它运行测试 API server/etcd;但 envtest 本身也没有完整 kubelet、scheduler 等运行集群。本文没有启用 envtest,因此只声称验证了分支与调用方式。v0.25.2 fake 包
RBAC 要和缓存范围、动作范围一起收窄
文末 Role 只允许在 demo 命名空间 get/list/watch/update ConfigMap,不包含 Secret、Pod、删除、集群管理员权限。RoleBinding 把它授给特定 ServiceAccount。若真正把 controller 部署进去,manager 的缓存观察范围也要限制在 demo,否则默认跨命名空间 list/watch 会被拒绝。Kubernetes RBAC
这个 Role 允许修改 demo 中的全部 ConfigMap,仍比“只改一个指定对象”更宽。实际系统可按功能拆命名空间或进一步限定读取条件与授权,但需考虑 list/watch 的约束;不能给所有 verbs 和 resources 写星号来消除报错。示例只演示 ConfigMap 业务权限,若启用 leader election,还需针对所选 namespace 和 Lease 配置单独评估权限,不能声称此 Role 已覆盖完整生产 manager。
本地程序没有加载 ServiceAccount 或 kubeconfig,RBAC YAML 也没有 apply。它之所以附在这里,是为了让“这个程序需要哪些外部权限”成为可审查的具体文件,而不是让测试通过替代权限设计。
版本选择要看组合,不是把每个包都升到 latest
2026-10-13 查阅的版本与本例选择如下。这个表是可核对的快照,阅读时不应当成永远最新的安装命令。
| 工具 | 官方资料快照或本例固定值 | 选择时还要核对 |
|---|---|---|
| Docker Engine | 29.9.0 发布说明 | CLI、daemon、BuildKit、存储后端与宿主条件 |
| Kubernetes | 发布页为 1.37.1 | 发行版支持、版本偏差、feature gates、CNI/CSI |
| Helm | v4.3.0 release | chart、插件和部署平台对主版本的兼容 |
| Argo CD | v3.5.5 release | 目标 Kubernetes、CRD、权限与升级说明 |
| GitHub Actions | 服务没有一个统一客户端版本;示例 action 需固定 SHA | runner 版本、action runtime 与仓库策略 |
| ko | v0.19.1 release | 本地 Go 工具链、基础镜像、平台与推送策略 |
| client-go | 本例 v0.37.1 | 与 k8s.io/api、apimachinery 同系列 |
| controller-runtime | 本例 v0.25.2 | 官方兼容表对应 client-go v0.37;最低 Go 1.26 |
controller-runtime 每个 minor 对应特定 client-go minor,跨 minor 兼容不能想当然。其 v0.25.2 go.mod 使用 k8s.io v0.37 系列;本例三个直接依赖统一为 v0.37.1,并以 Go 1.27.2 实际编译验证。库兼容和 server API 兼容又是两层:库能编译,不保证旧集群支持新字段或新资源。兼容表与 go.mod
最后可以用三个问题收敛选择:只差一个镜像,就先在 Dockerfile 和 ko 中选;只差可追踪发布,就建立 CI、不可变制品与部署声明;已经出现重复、复杂且可定义的运维状态转换,再考虑 controller-runtime。增加一个长期运行的控制器,意味着增加一份需要升级、告警和故障恢复的服务。
验证记录与限制
本例通过官方 Go 模块代理与 checksum 服务获取固定依赖,go.sum 随文保留。go mod verify 返回 all modules verified;go test -p 2 ./...、go test -p 2 -race ./...、go vet -p 2 ./... 均通过,go run -p 2 . 输出上文的两行结果。测试覆盖重复协调不重复写、保留已有键、对象缺失与注入冲突后的重新读取。程序没有注册 manager、watch 或队列,是直接调用 Reconcile 的单元级实验,不是一份已经运行的完整控制器。没有执行 Docker、ko、Helm、Argo CD 或 GitHub 工作流,没有调用任何真实 Kubernetes API,也没有创建资源或身份。
依据
- Docker 多阶段构建、Engine 29.9.0
- Kubernetes 组件、发布页、RBAC
- Helm template、Helm v4.3.0
- Argo CD 架构、Helm 集成、自动同步、v3.5.5
- GitHub Actions 安全
- ko、配置、静态资产、限制、v0.19.1
- client-go v0.37.1 README,以及本次下载的同版本 tools/cache、kubernetes/fake 源码。
- controller-runtime 兼容表、v0.25.2 go.mod、Reconcile 契约、FAQ、fake 文档。线上查阅日期 2026-10-13,源码与依赖均固定版本。
完整复现文件
保存以下文件。首次需从官方模块代理下载 go.mod/go.sum 锁定的依赖,可在该目录执行 GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download;示例不读集群凭据。已有缓存后,可离线运行 sh verify.sh。脚本将编译并行度设为 2,避免首次构建庞大客户端依赖时造成过高瞬时压力。RBAC 清单仅供审查,未部署;demo 命名空间与真实 controller 生命周期不在本实验中创建。
go.mod
代码块 · 64 行 · 点击收起 / 展开
go
module example.com/toolchoice
go 1.27.0
require (
k8s.io/api v0.37.1
k8s.io/apimachinery v0.37.1
k8s.io/client-go v0.37.1
sigs.k8s.io/controller-runtime v0.25.2
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.27.1 // indirect
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
github.com/go-openapi/swag/conv v0.27.1 // indirect
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
github.com/go-openapi/swag/loading v0.27.1 // indirect
github.com/go-openapi/swag/mangling v0.27.1 // indirect
github.com/go-openapi/swag/netutils v0.27.1 // indirect
github.com/go-openapi/swag/pools v0.27.1 // indirect
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.24.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.0 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)go.sum
代码块 · 169 行 · 点击收起 / 展开
text
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.37.1 h1:l6N77U7tjwB5L056bgrBTJIEdevac/naBZ3iSvDNfpM=
k8s.io/api v0.37.1/go.mod h1:zSlbB1YpJ1YQlFVQy20UYll81UJSJJUMLhkhvg6Z78M=
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE=
k8s.io/apimachinery v0.37.1/go.mod h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw=
k8s.io/client-go v0.37.1 h1:QTv/5ha4jAHtW9qxxVBkQVFBRDb4jHfFopQqqMdc+wM=
k8s.io/client-go v0.37.1/go.mod h1:dnAPtTnCNY38Ho04D2KdY1F4IKausa9UbqaAZKl60SY=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/controller-runtime v0.25.2 h1:bEkK3PVOIVK9X8QWLGVhJgmFc++47vfT6wakSzAOLsQ=
sigs.k8s.io/controller-runtime v0.25.2/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=main.go
代码块 · 72 行 · 点击收起 / 展开
go
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgofake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
runtimefake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
type Reconciler struct{ Client client.Client }
func (r Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
var cm corev1.ConfigMap
if err := r.Client.Get(ctx, req.NamespacedName, &cm); err != nil {
if apierrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
if cm.Data["managed"] == "true" {
return reconcile.Result{}, nil
}
if cm.Data == nil {
cm.Data = map[string]string{}
}
cm.Data["managed"] = "true"
return reconcile.Result{}, r.Client.Update(ctx, &cm)
}
type countingClient struct {
client.Client
updates int
}
func (c *countingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
c.updates++
return c.Client.Update(ctx, obj, opts...)
}
func main() {
ctx := context.Background()
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "demo"}, Data: map[string]string{"owner": "team"}}
cs := clientgofake.NewClientset(cm.DeepCopy())
list, err := cs.CoreV1().ConfigMaps("demo").List(ctx, metav1.ListOptions{})
if err != nil {
panic(err)
}
fmt.Println("client-go fake: configmaps=", len(list.Items))
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
panic(err)
}
c := &countingClient{Client: runtimefake.NewClientBuilder().WithScheme(scheme).WithObjects(cm.DeepCopy()).Build()}
r := Reconciler{Client: c}
req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "demo", Name: "app"}}
for range 2 {
if _, err := r.Reconcile(ctx, req); err != nil {
panic(err)
}
}
var got corev1.ConfigMap
if err := c.Get(ctx, req.NamespacedName, &got); err != nil {
panic(err)
}
fmt.Printf("two reconciles: updates=%d managed=%s owner=%s\n", c.updates, got.Data["managed"], got.Data["owner"])
}main_test.go
代码块 · 75 行 · 点击收起 / 展开
go
package main
import (
"context"
"errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
runtimefake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"testing"
)
func newClient(t *testing.T) client.Client {
t.Helper()
s := runtime.NewScheme()
if err := corev1.AddToScheme(s); err != nil {
t.Fatal(err)
}
return runtimefake.NewClientBuilder().WithScheme(s).WithObjects(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "demo"}, Data: map[string]string{"owner": "team"}}).Build()
}
func request(name string) reconcile.Request {
return reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "demo", Name: name}}
}
func TestIdempotent(t *testing.T) {
ctx := context.Background()
c := &countingClient{Client: newClient(t)}
r := Reconciler{Client: c}
for range 2 {
if _, err := r.Reconcile(ctx, request("app")); err != nil {
t.Fatal(err)
}
}
if c.updates != 1 {
t.Fatal(c.updates)
}
var cm corev1.ConfigMap
if err := c.Get(ctx, request("app").NamespacedName, &cm); err != nil {
t.Fatal(err)
}
if cm.Data["managed"] != "true" || cm.Data["owner"] != "team" {
t.Fatal(cm.Data)
}
if _, err := r.Reconcile(ctx, request("missing")); err != nil {
t.Fatal(err)
}
}
type conflictOnce struct {
client.Client
failed bool
}
func (c *conflictOnce) Update(ctx context.Context, o client.Object, opts ...client.UpdateOption) error {
if !c.failed {
c.failed = true
return apierrors.NewConflict(schema.GroupResource{Resource: "configmaps"}, o.GetName(), errors.New("injected conflict"))
}
return c.Client.Update(ctx, o, opts...)
}
func TestConflictIsReturnedThenReread(t *testing.T) {
ctx := context.Background()
c := &conflictOnce{Client: newClient(t)}
r := Reconciler{Client: c}
if _, err := r.Reconcile(ctx, request("app")); !apierrors.IsConflict(err) {
t.Fatalf("expected conflict: %v", err)
}
if _, err := r.Reconcile(ctx, request("app")); err != nil {
t.Fatal(err)
}
}rbac.yaml
yaml
# Unapplied teaching configuration. Namespace demo must already exist.
apiVersion: v1
kind: ServiceAccount
metadata:
name: config-reconciler
namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: config-reconciler
namespace: demo
rules:
- apiGroups: ['']
resources: [configmaps]
verbs: [get, list, watch, update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-reconciler
namespace: demo
subjects:
- kind: ServiceAccount
name: config-reconciler
namespace: demo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: config-reconcilerverify.sh
sh
#!/bin/sh
set -eu
cd "$(dirname "$0")"
go version
go mod verify
go test -p 2 ./...
go test -p 2 -race ./...
go vet -p 2 ./...
go run -p 2 .